Skip to content

Data Binding

When building web applications, we often want to sync a bit of state to a particular form input. For example, a "username" field should be bound to the value of a username state variable.

This is commonly known as “data binding”. Most front-end frameworks offer a way to bind a particular bit of state to a particular form control.

Here's what this typically looks like in React:

Code Playground

import React from 'react';

function SearchForm() {
const [searchTerm, setSearchTerm] = React.useState('cats');
return (
<>
<form>
<label htmlFor="search-input">
Search:
</label>
<input
type="text"
id="search-input"
value={searchTerm}
onChange={(event) => {
setSearchTerm(event.target.value);
}}
/>
</form>
<p>
Search term: {searchTerm}
</p>
</>
);
}

export default SearchForm;

If you'd like, take a few minutes and poke at this. What happens if you change or remove the value or onChange attributes?

Let's dig into it:

Video Summary

  • The value attribute works differently in React than it does in HTML.
    • In HTML, value sets the default value, and can be edited
    • In React, value locks the input to the specified value, and it becomes read-only.
  • By setting value equal to our searchTerm state variable, we ensure that the input will always display the search term.
  • Let's add a button that sets the search term to a random number. We see that by clicking this button, the input will be updated to show this new value.
  • Essentially, our data binding is 50% complete. The input will always show the value of the searchTerm state, even when that value is updated, but the state can't be changed by the input. The binding is only one way.
  • When we add an onChange handler, we see that the input actually does briefly update to show the new value. The problem is that React will undo that change immediately after the change event fires, before the browser has even had the time to complete a single repaint.
  • We can call setSearchTerm with the input's current value as a way to persist that edit. When React re-renders, the input will be updated to show the updated value held in state.
  • Do we really need value? It seems to work with just the onChange listener!
    • Well, this is also a one-way data binding, but it's the opposite of the situation we saw before. The input can update the state, but the state can't update the input. If we click our random number generator button, the state is updated to a new number, but the input doesn't update to show this new value.
    • Similarly, if our state has an initial value like cats, we won't see that initial value unless we control the input by setting value={searchTerm}.

Here's the sandbox from the end of the video:

Code Playground

import React from 'react';

function SearchForm() {
const [searchTerm, setSearchTerm] = React.useState('cats!');
return (
<>
<form>
<label htmlFor="search-input">
Search:
</label>
<input
type="text"
id="search-input"
value={searchTerm}
onChange={(event) => {
setSearchTerm(event.target.value);
}}
/>
</form>

<p>
Search term: {searchTerm}
</p>

<button
onClick={() => setSearchTerm(Math.random())}
>
Click me
</button>
</>
);
}

export default SearchForm;

Controlled vs. Uncontrolled inputs

When we set the value attribute on a form input, we tell React that it should be a controlled input. The word “controlled” has a specific definition in React; it means that React is managing the input.

By contrast, if we don't set value, the input is an uncontrolled input. This means that React doesn't do any management.

There's a golden rule here: An input should always either be controlled or uncontrolled. React doesn't like when we flip an element from one to the other.

This can lead to a common footgun. Let's learn about it, so that we can avoid it.

Consider this situation:

Code Playground

import React from 'react';

function SignupForm() {
// No default value:
const [username, setUsername] = React.useState();
return (
<form>
<label htmlFor="username">
Select a username:
</label>
<input
type="text"
id="username"
value={username}
onChange={event => {
setUsername(event.target.value);
}}
/>
</form>
);
}

export default SignupForm;
preview
console

Try typing in the text input, and then switch to the “Console” tab. You should see a warning that begins like this:

Warning: A component is changing an uncontrolled input to be controlled.

This is weird, right? This input is controlled! We're setting value={username} from the very first render!

Here's the problem: username is undefined at first, since there is no default value in the state hook. Here's a simplified version of what we're doing:

const username = undefined;
<input
type="text"
id="username"
value={username}
onChange={event => {
setUsername(event.target.value);
}}
/>

When we set value to undefined, it's the same as not setting it at all. React will treat the input as an uncontrolled input.

When the user starts typing in the input, the onChange event updates the value of username from undefined to a string. And so, React flips the element to a controlled input, and raises the warning.

Here's how to solve the problem: We always want to make sure we're passing a defined value. We can do this by initializing username to an empty string:

// 🚫 Incorrect. `username` will flip from `undefined` to a string:
const [username, setUsername] = React.useState();
// ✅ Correct. `username` will always be a string:
const [username, setUsername] = React.useState('');

With this change, our input is being controlled by React state from the very first render, since we're always passing a defined value. Even though empty strings are considered falsy 👀, they still “count” when it comes to controlling React inputs.